Project Overview
Project Goal
Build a "Task Manager" app with Firebase Authentication and Firestore. Users can sign up, sign in, create tasks, view their tasks, update tasks, and delete tasks. All data is stored in Firestore and synced in real-time.
Features to Implement
- User authentication (sign up, sign in, sign out)
- Create new tasks
- View list of tasks (real-time updates)
- Update task status (completed/pending)
- Delete tasks
- User-specific data (each user sees only their tasks)
Firebase Setup
Step 1: Create Firebase Project
- Go to https://console.firebase.google.com
- Click "Add project" and follow the setup wizard
- Enable Google Analytics (optional)
- Note your project ID
Step 2: Add Flutter App to Firebase
- In Firebase Console, click "Add app" → Flutter
- Register your app with package name (e.g., com.example.task_manager)
- Download configuration files:
google-services.jsonfor Android (place inandroid/app/)GoogleService-Info.plistfor iOS (place inios/Runner/)
Step 3: Enable Authentication
- In Firebase Console, go to Authentication → Sign-in method
- Enable "Email/Password" provider
- Save changes
Step 4: Set Up Firestore
- In Firebase Console, go to Firestore Database
- Click "Create database"
- Start in test mode (for development)
- Choose a location for your database
Project Setup
Step 1: Create Project
flutter create task_manager
cd task_manager
Step 2: Add Dependencies
Update pubspec.yaml:
pubspec.yaml
name: task_manager
description: A Flutter task manager app with Firebase
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
firebase_core: ^2.24.0
firebase_auth: ^4.15.0
cloud_firestore: ^4.13.0
cupertino_icons: ^1.0.6
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
Step 3: Install Firebase CLI and FlutterFire
# Install Firebase CLI
npm install -g firebase-tools
# Install FlutterFire CLI
dart pub global activate flutterfire_cli
# Configure Firebase
flutterfire configure
Step 4: Install Dependencies
flutter pub get
Project Structure
Suggested Folder Layout
lib/
main.dart
models/
task.dart
services/
auth_service.dart
task_service.dart
screens/
auth_screen.dart
home_screen.dart
add_task_screen.dart
widgets/
task_item.dart
loading_widget.dart
utils/
constants.dart
Creating Task Model
lib/models/task.dart
import 'package:cloud_firestore/cloud_firestore.dart';
class Task {
final String? id;
final String title;
final String description;
final bool isCompleted;
final DateTime createdAt;
final String userId;
Task({
this.id,
required this.title,
required this.description,
this.isCompleted = false,
required this.createdAt,
required this.userId,
});
factory Task.fromFirestore(Map data, String id) {
return Task(
id: id,
title: data['title'] as String,
description: data['description'] as String,
isCompleted: data['isCompleted'] as bool? ?? false,
createdAt: (data['createdAt'] as Timestamp).toDate(),
userId: data['userId'] as String,
);
}
Map toFirestore() {
return {
'title': title,
'description': description,
'isCompleted': isCompleted,
'createdAt': Timestamp.fromDate(createdAt),
'userId': userId,
};
}
Task copyWith({
String? id,
String? title,
String? description,
bool? isCompleted,
DateTime? createdAt,
String? userId,
}) {
return Task(
id: id ?? this.id,
title: title ?? this.title,
description: description ?? this.description,
isCompleted: isCompleted ?? this.isCompleted,
createdAt: createdAt ?? this.createdAt,
userId: userId ?? this.userId,
);
}
}
Creating Authentication Service
lib/services/auth_service.dart
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
// Get current user
User? get currentUser => _auth.currentUser;
// Auth state stream
Stream get authStateChanges => _auth.authStateChanges();
// Sign up with email and password
Future signUp(String email, String password) async {
try {
final credential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
return credential;
} on FirebaseAuthException catch (e) {
throw _handleAuthException(e);
}
}
// Sign in with email and password
Future signIn(String email, String password) async {
try {
final credential = await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
return credential;
} on FirebaseAuthException catch (e) {
throw _handleAuthException(e);
}
}
// Sign out
Future signOut() async {
await _auth.signOut();
}
// Handle auth exceptions
String _handleAuthException(FirebaseAuthException e) {
switch (e.code) {
case 'weak-password':
return 'The password provided is too weak.';
case 'email-already-in-use':
return 'The account already exists for that email.';
case 'user-not-found':
return 'No user found for that email.';
case 'wrong-password':
return 'Wrong password provided.';
case 'invalid-email':
return 'The email address is invalid.';
default:
return 'An error occurred: ${e.message}';
}
}
}
Creating Task Service
lib/services/task_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/task.dart';
class TaskService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final String _collection = 'tasks';
// Create a new task
Future createTask(Task task) async {
try {
await _firestore.collection(_collection).add(task.toFirestore());
} catch (e) {
throw Exception('Failed to create task: $e');
}
}
// Get all tasks for a user (stream for real-time updates)
Stream> getTasks(String userId) {
return _firestore
.collection(_collection)
.where('userId', isEqualTo: userId)
.orderBy('createdAt', descending: true)
.snapshots()
.map((snapshot) {
return snapshot.docs
.map((doc) => Task.fromFirestore(doc.data(), doc.id))
.toList();
});
}
// Update task
Future updateTask(Task task) async {
try {
await _firestore
.collection(_collection)
.doc(task.id)
.update(task.toFirestore());
} catch (e) {
throw Exception('Failed to update task: $e');
}
}
// Delete task
Future deleteTask(String taskId) async {
try {
await _firestore.collection(_collection).doc(taskId).delete();
} catch (e) {
throw Exception('Failed to delete task: $e');
}
}
// Toggle task completion status
Future toggleTaskCompletion(Task task) async {
try {
await _firestore
.collection(_collection)
.doc(task.id)
.update({'isCompleted': !task.isCompleted});
} catch (e) {
throw Exception('Failed to update task: $e');
}
}
}
Creating Authentication Screen
lib/screens/auth_screen.dart
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
class AuthScreen extends StatefulWidget {
@override
_AuthScreenState createState() => _AuthScreenState();
}
class _AuthScreenState extends State {
final _formKey = GlobalKey();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _authService = AuthService();
bool _isLogin = true;
bool _isLoading = false;
String? _error;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
if (_isLogin) {
await _authService.signIn(
_emailController.text.trim(),
_passwordController.text,
);
} else {
await _authService.signUp(
_emailController.text.trim(),
_passwordController.text,
);
}
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isLogin ? 'Sign In' : 'Sign Up'),
),
body: Padding(
padding: EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_isLogin ? 'Welcome Back' : 'Create Account',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
),
SizedBox(height: 32),
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email';
}
if (!value.contains('@')) {
return 'Please enter a valid email';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
if (value.length < 6) {
return 'Password must be at least 6 characters';
}
return null;
},
),
if (_error != null) ...[
SizedBox(height: 16),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red),
),
child: Row(
children: [
Icon(Icons.error, color: Colors.red),
SizedBox(width: 8),
Expanded(
child: Text(
_error!,
style: TextStyle(color: Colors.red),
),
),
],
),
),
],
SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _isLoading ? null : _submit,
child: _isLoading
? CircularProgressIndicator(color: Colors.white)
: Text(_isLogin ? 'Sign In' : 'Sign Up'),
),
),
SizedBox(height: 16),
TextButton(
onPressed: () {
setState(() {
_isLogin = !_isLogin;
_error = null;
});
},
child: Text(
_isLogin
? 'Don\'t have an account? Sign Up'
: 'Already have an account? Sign In',
),
),
],
),
),
),
);
}
}
Creating Task Item Widget
lib/widgets/task_item.dart
import 'package:flutter/material.dart';
import '../models/task.dart';
class TaskItem extends StatelessWidget {
final Task task;
final VoidCallback onToggle;
final VoidCallback onDelete;
final VoidCallback onTap;
const TaskItem({
Key? key,
required this.task,
required this.onToggle,
required this.onDelete,
required this.onTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: Checkbox(
value: task.isCompleted,
onChanged: (_) => onToggle(),
),
title: Text(
task.title,
style: TextStyle(
decoration: task.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
color: task.isCompleted ? Colors.grey : null,
),
),
subtitle: Text(
task.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: onDelete,
),
onTap: onTap,
),
);
}
}
Creating Home Screen
lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../models/task.dart';
import '../services/auth_service.dart';
import '../services/task_service.dart';
import '../widgets/task_item.dart';
import 'add_task_screen.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final authService = AuthService();
final taskService = TaskService();
final user = authService.currentUser;
if (user == null) {
return Center(child: Text('No user logged in'));
}
return Scaffold(
appBar: AppBar(
title: Text('My Tasks'),
actions: [
IconButton(
icon: Icon(Icons.logout),
onPressed: () async {
await authService.signOut();
},
tooltip: 'Sign Out',
),
],
),
body: StreamBuilder>(
stream: taskService.getTasks(user.uid),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
final tasks = snapshot.data ?? [];
if (tasks.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.task_alt, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'No tasks yet',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
SizedBox(height: 8),
Text(
'Tap the + button to add a task',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
);
}
return ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return TaskItem(
task: task,
onToggle: () => taskService.toggleTaskCompletion(task),
onDelete: () async {
final confirm = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Delete Task'),
content: Text('Are you sure you want to delete this task?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Delete', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirm == true) {
await taskService.deleteTask(task.id!);
}
},
onTap: () {
// Navigate to edit screen (optional)
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddTaskScreen(task: task),
),
);
},
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddTaskScreen(),
),
);
},
child: Icon(Icons.add),
tooltip: 'Add Task',
),
);
}
}
Creating Add/Edit Task Screen
lib/screens/add_task_screen.dart
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../models/task.dart';
import '../services/task_service.dart';
class AddTaskScreen extends StatefulWidget {
final Task? task;
const AddTaskScreen({Key? key, this.task}) : super(key: key);
@override
_AddTaskScreenState createState() => _AddTaskScreenState();
}
class _AddTaskScreenState extends State {
final _formKey = GlobalKey();
final _titleController = TextEditingController();
final _descriptionController = TextEditingController();
final _taskService = TaskService();
bool _isLoading = false;
@override
void initState() {
super.initState();
if (widget.task != null) {
_titleController.text = widget.task!.title;
_descriptionController.text = widget.task!.description;
}
}
@override
void dispose() {
_titleController.dispose();
_descriptionController.dispose();
super.dispose();
}
Future _saveTask() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
});
try {
final user = FirebaseAuth.instance.currentUser;
if (user == null) {
throw Exception('User not logged in');
}
final task = Task(
id: widget.task?.id,
title: _titleController.text.trim(),
description: _descriptionController.text.trim(),
isCompleted: widget.task?.isCompleted ?? false,
createdAt: widget.task?.createdAt ?? DateTime.now(),
userId: user.uid,
);
if (widget.task == null) {
await _taskService.createTask(task);
} else {
await _taskService.updateTask(task);
}
Navigator.pop(context);
} catch (e) {
setState(() {
_isLoading = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.task == null ? 'Add Task' : 'Edit Task'),
),
body: Padding(
padding: EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _titleController,
decoration: InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.title),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a title';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.description),
),
maxLines: 4,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a description';
}
return null;
},
),
SizedBox(height: 24),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: _isLoading ? null : _saveTask,
child: _isLoading
? CircularProgressIndicator(color: Colors.white)
: Text(widget.task == null ? 'Create Task' : 'Update Task'),
),
),
],
),
),
),
);
}
}
Updating Main.dart
lib/main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'screens/auth_screen.dart';
import 'screens/home_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Task Manager',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: AuthWrapper(),
);
}
}
class AuthWrapper extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasData) {
return HomeScreen();
}
return AuthScreen();
},
);
}
}
Firestore Security Rules
Basic Security Rules
Update Firestore rules in Firebase Console → Firestore Database → Rules:
Firestore Rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /tasks/{taskId} {
// Users can only read/write their own tasks
allow read, write: if request.auth != null
&& request.auth.uid == resource.data.userId;
// Allow creation if userId matches authenticated user
allow create: if request.auth != null
&& request.auth.uid == request.resource.data.userId;
}
}
}
Project Checklist
Implementation Checklist
- ✅ Firebase project created and configured
- ✅ Authentication enabled (Email/Password)
- ✅ Firestore database created
- ✅ Flutter project created with Firebase dependencies
- ✅ Task model with Firestore serialization
- ✅ Authentication service implemented
- ✅ Task service with CRUD operations
- ✅ Authentication screen (sign up/sign in)
- ✅ Home screen with real-time task list
- ✅ Add/Edit task screen
- ✅ Task item widget with actions
- ✅ Security rules configured
- ✅ Main.dart with auth state management
Testing the App
Test Steps
- Run the app:
flutter run - Sign up with a new email and password
- Create a few tasks
- Toggle task completion status
- Edit a task
- Delete a task
- Sign out and sign in with the same credentials
- Verify tasks persist and are user-specific
Enhancement Ideas
Optional Enhancements
- Add task categories/tags
- Implement task due dates and reminders
- Add task priority levels
- Implement task search and filtering
- Add task sharing between users
- Implement task attachments (images, files)
- Add dark mode support
- Implement offline support with local caching
Exercises
1. Complete Implementation
Implement the entire Firebase CRUD app following all the code provided. Test authentication, create, read, update, and delete operations. Verify that users can only see their own tasks.
2. Add Task Categories
Extend the Task model to include a category field. Add a category selector in the add/edit task screen. Implement filtering by category in the home screen.
3. Add Due Dates
Add a due date field to tasks. Display overdue tasks with a different color. Add date picker in the add/edit task screen.